nerve config validate — CI-ready config bundle validation (7/13) - #210
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds CI-friendly configuration bundle validation (nerve config validate) and supporting validation primitives to hard-fail on structurally invalid config bundles (cron schedules, gate specs, wrong-shaped layers, unsafe cwd-resolving paths), while remaining intentionally lenient about unset ${ENV_VAR} secrets unless explicitly requested.
Changes:
- Introduces
validate_config_bundle()with detailed reporting and a newnerve config validateCLI command. - Tightens cron schedule and gate-spec validation (including new interval parsing semantics and gate spec field allowlists via
spec_keys). - Expands test coverage and documentation for validation behavior, strictness flags, and plugin non-execution guarantees.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/test_cron_gates.py | Adds regression tests for gate-shape normalization/logging behavior. |
| tests/test_config_validate.py | Adds comprehensive tests for config-bundle validation semantics and CLI behavior. |
| tests/test_config_sources.py | Ensures CLI emits clean errors for config mistakes (including bad backend) while preserving tracebacks for internal defects. |
| nerve/cron/service.py | Splits interval parsing to expose _interval_seconds() for validation without changing daemon fallback behavior. |
| nerve/cron/jobs.py | Prevents accidental normalization of wrong-shaped run_if/skip_when_idle, and logs+ignores invalid shapes instead of raising. |
| nerve/cron/gates.py | Adds spec_keys allowlists and improves build_gate() robustness for non-string/unhashable type. |
| nerve/config_validate.py | New validation module: layer reading, cwd-path footgun checks, cron/gate/schedule validation, and reporting. |
| nerve/cli.py | Adds nerve config validate command and refines “clean error vs traceback” behavior in the CLI group callback. |
| docs/cron.md | Documents schedule fallback vs validation behavior, and clarifies that validation does not execute plugin gates. |
| docs/config.md | Adds a full “Validating Configuration” section including CI guidance and flags. |
| config.example.yaml | Updates cron comments and documents cron.auto_reload example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 28, 2026 12:11
726ca79 to
12c7340
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 28, 2026 12:36
12c7340 to
8c7a5fd
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 28, 2026 13:14
8c7a5fd to
e171c79
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 28, 2026 14:23
e171c79 to
a4016f2
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 28, 2026 14:54
a4016f2 to
09a9260
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
2 times, most recently
from
July 28, 2026 16:03
b923d11 to
aa8026e
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
2 times, most recently
from
July 28, 2026 18:40
2c7f820 to
ee55d3e
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 29, 2026 08:29
ee55d3e to
51b1ebb
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
3 times, most recently
from
July 29, 2026 10:31
8947dbc to
e165653
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 29, 2026 13:28
e165653 to
37d5fa6
Compare
alex-clickhouse
marked this pull request as ready for review
July 29, 2026 15:25
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 30, 2026 08:01
37d5fa6 to
7a5ffb6
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 30, 2026 10:06
7a5ffb6 to
51fb706
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 31, 2026 08:19
51fb706 to
8da134a
Compare
alex-clickhouse
force-pushed
the
config-refactor/07-validate
branch
from
July 31, 2026 08:36
8da134a to
2826475
Compare
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:33
2826475 to
d2fc670
Compare
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:34
d2fc670 to
436cb05
Compare
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:46
436cb05 to
e710dfc
Compare
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:58
e710dfc to
7f14e69
Compare
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:58
7f14e69 to
5439ec5
Compare
Hard-failing validation over a whole bundle — unknown keys, cron job and gate
parsing, source sanity and ${VAR} resolvability — with a non-zero exit so it
drops straight into a config repo's CI.
Validation reads and parses; it never loads the bundle's own code. Importing a
gate plugin to check it would mean the bundle had already executed by the time we
decided it was unfit, so an unrecognized gate type is reported as unverified
rather than accepted or rejected. Built-in gate specs are built, which is what
type-checking a gate spec means.
Two silent-acceptance gaps are closed. A blank path setting is refused with a
message saying what it would actually do: cron.gate_plugins_dir set to "" is not
"no directory" but the daemon's working directory, every .py in which is imported
and executed at startup and on every cron reload. And a schedule the scheduler
will not run now fails CI — a bad crontab, a string that is neither crontab nor
interval, a zero interval, or a non-string that would raise past every handler
the daemon has. Source schedules were not checked at all and now are, through the
same helper the daemon uses, so the validator and the runtime cannot disagree
about what is valid.
Co-Authored-By: Claude <noreply@anthropic.com>
The validator refused a blank `workspace`, `cron.jobs_file`,
`cron.system_file` or `cron.gate_plugins_dir` on the premise that
`Path("")` is `Path(".")`, so a blank value "means the directory the
daemon happened to be started in". It does not. `_expand_path` maps
blank to None before it can become `Path(".")`, and the field takes its
documented default — which the validator's own run demonstrated, logging
the resolved default path and then erroring to say the value resolved to
the working directory.
So the same bundle loaded correctly and failed CI. The shape that hits it
is `jobs_file: ${CRON_JOBS_FILE:-}`, and docs/config.md promotes
`${VAR:-default}` as the fleet idiom. `docs/config.md` also carried both
readings, in two places, and the tests pinned both contracts with
`TestBlankPathSettingsMeanUnset` and `TestWorkingDirectoryPaths` green
side by side.
Blank is no longer reported. The explicit `.` / `./` / `./.` check stays
a hard error, because those do survive the loader as the path they are.
The rule was also a hand-written four-key literal against fourteen path
settings, so `gateway.ssl.cert` accepted a dot that switches TLS on with
a directory as the certificate. The keys are now derived from the config
dataclasses: one test walks every `Path`-annotated field and fails if it
is neither covered nor exempted with a reason, a second fails if the rule
names a field the config does not declare, and a third asserts each
covered key actually produces an error, so a key cannot be listed and
never read. Six settings that check turned up are now covered.
Co-Authored-By: Claude <noreply@anthropic.com>
pufit
force-pushed
the
config-refactor/07-validate
branch
from
August 1, 2026 02:58
5439ec5 to
77b7855
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
nerve config validatechecks a whole config bundle and exits non-zero on any error, so it drops straight into CI on a config repo. It covers unknown keys, cron job and gate parsing, source sanity,${VAR}resolvability, and schedules the scheduler would not actually run as written.--portable-onlydrops the machine-local layers and judges the tracked layer alone.--strict-keysand--strict-envpromote warnings to errors. It runs even when the config cannot otherwise load, which is the point.Why
Every class of error here was previously found by a daemon at start-up, after the change had already merged and synced — leaving the instance on its old config with a log line nobody was reading. Moving the check before the merge is the whole value.
Schedules are the clearest case. A 5-field crontab the scheduler rejects (
99 * * * *) is refused at run time and logged. A schedule that is neither a crontab nor an interval (hourly,@daily) is never complained about at run time at all — it silently never fires.--portable-onlyexists because a machine-local override can mask an invalid shared value, and, more commonly, a broken local file condemns a shared bundle with nothing wrong with it. Validating the tracked layer on its own is the only question whose answer does not depend on which host ran the check.Validation reads and parses; it does not execute candidate gate-plugin code. "An invalid bundle is refused" is not a guarantee you can make about code you had to run in order to judge it, so an unrecognised gate type is reported as unverified rather than accepted or rejected. Testing a plugin is the author's job.
Added in review
The validator rejected blank path values that the loader accepts. It refused
a blank
workspace,cron.jobs_file,cron.system_fileorcron.gate_plugins_diron the stated premise thatPath("")isPath("."), soa blank "means the directory the daemon happened to be started in". It does not:
_expand_pathmaps blank toNonebefore it can becomePath("."), and thefield takes its documented default. A single validator run showed both halves —
the log line resolving the blank to the default, then the error saying it
resolved to the working directory.
The shape that hits it is
jobs_file: ${CRON_JOBS_FILE:-}, and the docs promote${VAR:-default}as the fleet idiom.docs/config.mdcarried both readings intwo places, and the suite pinned both contracts with
TestBlankPathSettingsMeanUnsetand
TestWorkingDirectoryPathsgreen side by side.Blank is no longer reported. An explicit
././/./.stays a hard error,because those do survive the loader as the path they are.
The rule was also a hand-written four-key literal against fourteen path settings,
so
gateway.ssl.certaccepted a dot that switches TLS on with a directory as thecertificate. The keys are now derived from the config dataclasses: one test walks
every
Path-annotated field and fails if it is neither covered nor exempted witha reason, a second fails if the rule names a field the config does not declare,
and a third asserts each covered key actually produces an error — so a key cannot
be listed and never read. Six settings that check turned up are now covered.
🤖 Generated with Claude Code